GPU Path Tracing

Created by miccall (转载请注明出处 miccall.tech)

1. OnRenderImage:

这个方法是unity在camera执行渲染时候被调用的一个 callback

        private void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
            SetShaderParameters();
            Render(destination);
        }

我们在这里执行两种操作,第一个是把我们定义的值传入到 computer shader 中让GPU执行,
执行SetShaderParameters 就是 向 computer shader 传入参数 。

传入的参数有


    RWTexture2D<float4> Result;
    float4x4 _CameraToWorld;
    float4 _DirectionalLight;
    float4x4 _CameraInverseProjection;
    Texture2D <float4> _SkyboxTexture;
    SamplerState sampler_SkyboxTexture;
    float2 _PixelOffset;
    float _Seed;

  1. Result 是一个可读写的图 ,由compute shader计算得到
  2. _CameraToWorld 是相机空间到世界空间的转化矩阵,我们为了方便,由自定义传入,而不是直接在shader里面计算
  3. _DirectionalLight 是光照方向,也是由世界空间的灯光信息传入
  4. _CameraInverseProjection 投影矩阵的逆矩阵,用于把相机空间的方向转化到世界空间
  5. _SkyboxTexture 是天空盒贴图
  6. _PixelOffset 我们设置的一个随机偏移量
  7. _Seed 是随机种子

有一部分我们可以预处理的时候传入,一部分我们要做一些运算才能传入:

        private void SetShaderParameters()
        {
            RayTracingShader.SetFloat("_Seed", Random.value);
            RayTracingShader.SetMatrix("_CameraToWorld", _camera.cameraToWorldMatrix);
            RayTracingShader.SetMatrix("_CameraInverseProjection", _camera.projectionMatrix.inverse);
            RayTracingShader.SetTexture(0, "_SkyboxTexture", skyboxTexture);
            RayTracingShader.SetVector("_PixelOffset", new Vector2(Random.value, Random.value));

            // light dir ;
            var l = DirectionalLight.transform.forward;
            RayTracingShader.SetVector("_DirectionalLight", new Vector4(l.x, l.y, l.z, DirectionalLight.intensity ));

            // ****** important **********
            RayTracingShader.SetBuffer(0, "_Spheres", _sphereBuffer);
        }

第二个是渲染一个纹理 ,这个纹理最终会被camera 展示出来 。
Render 渲染一张图 :


        private void Render(RenderTexture destination)
        {
            // current render target
            InitRenderTexture();


            // Set the target and dispatch to the compute shader
            RayTracingShader.SetTexture(0, "Result", _target);
            var threadGroupsX = Mathf.CeilToInt(Screen.width / 8.0f);
            var threadGroupsY = Mathf.CeilToInt(Screen.height / 8.0f);
            RayTracingShader.Dispatch(0, threadGroupsX, threadGroupsY, 1);



            // Blit the result texture to the screen
            if (_addMaterial == null)
                _addMaterial = new Material(Shader.Find("Hidden/AddShader"));
            _addMaterial.SetFloat(Sample, _currentSample);


            // 通过 addshader 来做一个 抗锯齿  
            Graphics.Blit(_target, _converged, _addMaterial);
            Graphics.Blit(_converged, destination);
            _currentSample++;

        }

2. compute shader path tracing

  1. Ray and create Ray

CreateCameraRay


struct Ray
{
    float3 origin;
    float3 direction;
    float3 energy;
};

Ray CreateRay(float3 origin, float3 direction)
{
    Ray ray;
    ray.origin = origin;
    ray.direction = direction;
    ray.energy = float3(1.0f, 1.0f, 1.0f);
    return ray;
}

Ray CreateCameraRay(float2 uv)
{
    // Transform the camera origin to world space
    float3 origin = mul(_CameraToWorld, float4(0.0f, 0.0f, 0.0f, 1.0f)).xyz;

    // 反转 view space  透视投影
    float3 direction = mul(_CameraInverseProjection, float4(uv, 0.0f, 1.0f)).xyz;
    // 将方向从摄 camera space 转换为  world space  并 normalize
    direction = mul( _CameraToWorld, float4(direction, 0.0f)).xyz;
    direction = normalize(direction);

    return CreateRay(origin, direction);
}

CreateCameraRay 只会在最开始的时候调用一次,其余的光线追踪时反弹的光线我们直接用 CreateRay

  1. RayHit and CreateRayHit

struct RayHit
{
    float3 position;
    float distance;
    float3 normal;
    float3 albedo;
    float3 specular;
    float smoothness;
    float3 emission;
};

RayHit CreateRayHit()
{
    RayHit hit;
    hit.position = float3(0.0f, 0.0f, 0.0f);
    hit.distance = 1.#INF;
    hit.normal = float3(0.0f, 0.0f, 0.0f);
    hit.specular = float3(0.04f, 0.04f, 0.04f);
    hit.albedo = min(1.0f - hit.specular, float3(0.8f, 0.8f, 0.8f));
    hit.smoothness = 1.0f;
    hit.emission = float3(0.0f,0.0f,0.0f);
    return hit;
}

RayHit 代表光线击中的点 ,在 CreateRayHit 中做了初始化,在后续的过程中会被重新计算

  1. Trace and Shade

RayHit Trace(Ray ray)
{
    RayHit bestHit = CreateRayHit();
    IntersectGroundPlane(ray, bestHit);
    uint numSpheres, stride;
    _Spheres.GetDimensions(numSpheres, stride);
    for (uint i = 0; i < numSpheres; i++)
        IntersectSphere(ray, bestHit, i);

    return bestHit;
}

从摄像机的每个像素发射一条射线 , 我们得到一个默认的击中点
IntersectGroundPlane 和 IntersectSphere 分别是求交算法,获得一条光线对地面和球的击中点信息 。
_Spheres 是 一个球的结构体 list ,里面存放着我们在外层 传入的球体数量和位置,大小等信息 。
我们循环便利这个list ,对每一个球都求交算出 besthit 点


float3 Shade(inout Ray ray, RayHit hit)
{
    if (hit.distance < 1.#INF)
    {
        // 有限距离 
    }
    else
    {   
        // 无限远 
    }
}

在有限距离中,我们对射中的物体顶点着色


        // Calculate chances of diffuse and specular reflection

        hit.albedo = min(1.0f - hit.specular, hit.albedo);
        float specChance = energy(hit.specular);
        float diffChance = energy(hit.albedo);

        // Roulette-select the ray's path
        float roulette = rand();
        if (roulette < specChance)
        {
            // Specular reflection
            ray.origin = hit.position + hit.normal * 0.001f;
            float alpha = SmoothnessToPhongAlpha(hit.smoothness);
            ray.direction = SampleHemisphere(reflect(ray.direction, hit.normal), alpha);
            float f = (alpha + 2) / (alpha + 1);
            ray.energy *= (1.0f / specChance) * hit.specular * sdot(hit.normal, ray.direction, f);
        }
        else if (diffChance > 0 && roulette < specChance + diffChance)
        {
            // Diffuse reflection
            ray.origin = hit.position + hit.normal * 0.001f;
            ray.direction = SampleHemisphere(hit.normal, 1.0f);
            ray.energy *= (1.0f / diffChance) * hit.albedo;
        }
        else
        {
            // Terminate ray
            ray.energy = 0.0f;
        }
        return hit.emission;

albedo 的 值是 从预先的固有色拿出的,但是为了避免 他的他的反射太强 ,如果他反射太强,就会盖过他的固有色。
所以我们用了一个min去做判定 。

计算 energy


float energy(float3 color)
{
    return dot(color, 1.0f / 3.0f);
}

能量的计算就是把rgb三个通道做一个平均。

Roulette-select 算法是一种加速的shading 方法,对 Specular reflection 和 Diffuse reflection 采用不同的贡献值去分开处理,而不是全部

其中我们要做的就是更新光线信息,并且对能量进行衰减 。还有要做的事情是:我们需要在半球上均匀分布的随机方向来更新光线方向 。


    float3x3 GetTangentSpace( float3 normal )
    {
        // Choose a helper vector for the cross product
        float3 helper = float3(1, 0, 0);
        if (abs(normal.x) > 0.99f)
            helper = float3(0, 0, 1);
        // Generate vectors
        float3 tangent = normalize(cross(normal, helper));
        float3 binormal = normalize(cross(normal, tangent));
        return float3x3(tangent, binormal, normal);
    }

    float3 SampleHemisphere( float3 normal, float alpha)
    {
        // Sample the hemisphere, where alpha determines the kind of the sampling
        float cosTheta = pow(rand(), 1.0f / (alpha + 1.0f));
        float sinTheta = sqrt(1.0f - cosTheta * cosTheta);
        float phi = 2 * PI * rand();
        float3 tangentSpaceDir = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
        // Transform direction to world space
        return mul(tangentSpaceDir, GetTangentSpace(normal));
    }

3. path tracing all path loop

  1. 从屏幕uv坐标

    _Pixel = id.xy;
    uint width, height;
    Result.GetDimensions(width, height);
    // Transform pixel to [-1,1] range 
    float2 uv = float2((id.xy + _PixelOffset ) / float2(width, height) * 2.0f - 1.0f);

为了对每个像素做多点采样 我们再每个uv坐标内进行了多点的随机采样

其中的 _PixelOffset 是由 c# 层 随机值传入的

RayTracingShader.SetVector("_PixelOffset", new Vector2(Random.value, Random.value));
  1. 创建uv坐标的光线

    // Get a ray for the UVs
    Ray ray = CreateCameraRay(uv);
    // Write some colors
    float3 result = float3(0, 0, 0);

  1. 反射次数:

    // 反射次数 
    for (int i = 0; i < 8 ; i++)
    {
        RayHit hit = Trace(ray); // 最开始的ray 是从 Camera 开始的 

        // shade的 ray 和 hit 都是 inout 参数  
        result += ray.energy * Shade(ray, hit);

        // 光线最多反射循环次数 ,如果中间 ray的 energy 的 xyz 有一个为 0 了 ,则它不会再反弹了 
        if (!any(ray.energy))
            break;
    }